Micron Document
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| SparkN0de-git | SparkN0de |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


Commit 9f04aef66ac475c8cd9781f372c202eb4a1374eb


Parents : 6bc49c4
Author : Ivan <ivan@quad4.io>
Signature : Signature validation error
Date : 2026-05-09T00:56:33-05:00

refactor(meshchat): implement delayed hangup for rejected calls and improve contact lookup handling

Changes

3 files changed, 73 insertions(+), 19 deletions(-)


Diff

diff --git a/meshchatx/meshchat.py b/meshchatx/meshchat.py
index 6f97b4ab..1367e5f9 100644
--- a/meshchatx/meshchat.py
+++ b/meshchatx/meshchat.py
@@ -2932,8 +2932,12 @@ class ReticulumMeshChat:
# Reject all calls if telephony is disabled
if not ctx.config.telephone_enabled.get():
- if ctx.telephone_manager.telephone:
- ctx.telephone_manager.telephone.hangup()
+ telephone = getattr(ctx.telephone_manager, "telephone", None)
+ if telephone:
+ threading.Timer(
+ 0.5,
+ lambda t=telephone: t.hangup(),
+ ).start()
return
if ctx.telephone_manager and ctx.telephone_manager.initiation_status:
@@ -2947,18 +2951,24 @@ class ReticulumMeshChat:
# Check if caller is blocked
if self.is_destination_blocked(caller_hash, context=ctx):
print(f"Rejecting incoming call from blocked source: {caller_hash}")
- if ctx.telephone_manager.telephone:
- ctx.telephone_manager.telephone.hangup()
+ telephone = getattr(ctx.telephone_manager, "telephone", None)
+ if telephone:
+ # Use a small delay to avoid deadlocking with LXST call_handler_lock
+ threading.Timer(
+ 0.5,
+ lambda t=telephone: t.hangup(),
+ ).start()
return
# Check for Do Not Disturb
if ctx.config.do_not_disturb_enabled.get():
print(f"Rejecting incoming call due to Do Not Disturb: {caller_hash}")
- if ctx.telephone_manager.telephone:
+ telephone = getattr(ctx.telephone_manager, "telephone", None)
+ if telephone:
# Use a small delay to ensure LXST state is ready for hangup
threading.Timer(
0.5,
- lambda: ctx.telephone_manager.telephone.hangup(),
+ lambda t=telephone: t.hangup(),
).start()
return
@@ -2967,13 +2977,21 @@ class ReticulumMeshChat:
ctx.config.telephone_allow_calls_from_contacts_only.get()
or ctx.config.block_all_from_strangers.get()
):
- contact = ctx.database.contacts.get_contact_by_identity_hash(caller_hash)
+ contact = None
+ try:
+ contact = ctx.database.contacts.get_contact_by_identity_hash(
+ caller_hash
+ )
+ except Exception:
+ # Treat lookup failure as non-contact to avoid accidentally allowing spam
+ pass
if not contact:
print(f"Rejecting incoming call from non-contact: {caller_hash}")
- if ctx.telephone_manager.telephone:
+ telephone = getattr(ctx.telephone_manager, "telephone", None)
+ if telephone:
threading.Timer(
0.5,
- lambda: ctx.telephone_manager.telephone.hangup(),
+ lambda t=telephone: t.hangup(),
).start()
return
@@ -2983,6 +3001,13 @@ class ReticulumMeshChat:
print(f"on_incoming_telephone_call: {caller_identity.hash.hex()}")
ch = caller_identity.hash.hex()
caller_name = (self.get_name_for_identity_hash(ch) or "").strip() or "Mesh"
+ is_contact = False
+ try:
+ is_contact = (
+ ctx.database.contacts.get_contact_by_identity_hash(ch) is not None
+ )
+ except Exception:
+ pass
AsyncUtils.run_async(
self.websocket_broadcast(
json.dumps(
@@ -2990,6 +3015,7 @@ class ReticulumMeshChat:
"type": "telephone_ringing",
"remote_identity_hash": ch,
"remote_identity_name": caller_name,
+ "is_contact": is_contact,
},
),
),
@@ -3079,10 +3105,14 @@ class ReticulumMeshChat:
if ctx.config.do_not_disturb_enabled.get():
is_filtered = True
elif ctx.config.telephone_allow_calls_from_contacts_only.get():
- contact = ctx.database.contacts.get_contact_by_identity_hash(
- remote_identity_hash,
- )
- if not contact:
+ try:
+ contact = ctx.database.contacts.get_contact_by_identity_hash(
+ remote_identity_hash,
+ )
+ if not contact:
+ is_filtered = True
+ except Exception:
+ # Treat lookup failure as filtered to avoid leaking missed-call noise
is_filtered = True
if not is_filtered:

diff --git a/meshchatx/src/frontend/components/App.vue b/meshchatx/src/frontend/components/App.vue
index 71c51e76..3fc4cfd4 100644
--- a/meshchatx/src/frontend/components/App.vue
+++ b/meshchatx/src/frontend/components/App.vue
@@ -1174,7 +1174,9 @@ export default {
if (this.config?.do_not_disturb_enabled) {
break;
}
- // If we are the caller (outgoing initiation), skip playing the incoming ringtone
+ if (this.config?.telephone_allow_calls_from_contacts_only && !json.is_contact) {
+ break;
+ }
if (this.initiationStatus) {
break;
}

diff --git a/tests/backend/test_incoming_call_policy.py b/tests/backend/test_incoming_call_policy.py
index 9b1b5101..9b8df110 100644
--- a/tests/backend/test_incoming_call_policy.py
+++ b/tests/backend/test_incoming_call_policy.py
@@ -39,10 +39,22 @@ def _run_incoming(app, caller, ctx=None):
bound(caller, context=ctx)
-def test_incoming_rejects_when_blocked_immediate_hangup(policy_app):
+def test_incoming_rejects_when_blocked_uses_delayed_hangup(policy_app):
policy_app.is_destination_blocked.return_value = True
caller = _caller_identity()
- _run_incoming(policy_app, caller)
+
+ with patch("meshchatx.meshchat.threading.Timer") as mock_timer:
+
+ def run_timer(delay, fn):
+ assert delay == 0.5
+ fn()
+ t = MagicMock()
+ t.start = MagicMock()
+ return t
+
+ mock_timer.side_effect = run_timer
+
+ _run_incoming(policy_app, caller)
policy_app.telephone_manager.telephone.hangup.assert_called_once()
policy_app.voicemail_manager.handle_incoming_call.assert_not_called()
@@ -127,9 +139,14 @@ def test_contacts_only_accepts_matching_contact(policy_app):
async_utils.run_async = MagicMock()
_run_incoming(policy_app, caller)
- policy_app.current_context.database.contacts.get_contact_by_identity_hash.assert_called_once_with(
+ # Called twice: once for policy check, once for websocket broadcast is_contact flag
+ policy_app.current_context.database.contacts.get_contact_by_identity_hash.assert_called_with(
CALLER_HASH_HEX,
)
+ assert (
+ policy_app.current_context.database.contacts.get_contact_by_identity_hash.call_count
+ == 2
+ )
policy_app.current_context.voicemail_manager.handle_incoming_call.assert_called_once_with(
caller
)
@@ -178,7 +195,10 @@ def test_when_policy_off_stranger_rings(policy_app):
async_utils.run_async = MagicMock()
_run_incoming(policy_app, caller)
- policy_app.current_context.database.contacts.get_contact_by_identity_hash.assert_not_called()
+ # Called once for websocket broadcast is_contact flag even when policy is off
+ policy_app.current_context.database.contacts.get_contact_by_identity_hash.assert_called_once_with(
+ CALLER_HASH_HEX,
+ )
policy_app.current_context.voicemail_manager.handle_incoming_call.assert_called_once_with(
caller
)
@@ -224,8 +244,10 @@ def test_uses_passed_context_not_app_current_context(policy_app):
async_utils.run_async = MagicMock()
_run_incoming(policy_app, caller, ctx=other_ctx)
- other_ctx.database.contacts.get_contact_by_identity_hash.assert_called_once_with(
+ # Called twice: once for policy check, once for websocket broadcast is_contact flag
+ other_ctx.database.contacts.get_contact_by_identity_hash.assert_called_with(
CALLER_HASH_HEX,
)
+ assert other_ctx.database.contacts.get_contact_by_identity_hash.call_count == 2
policy_app.current_context.database.contacts.get_contact_by_identity_hash.assert_not_called()
other_ctx.voicemail_manager.handle_incoming_call.assert_called_once_with(caller)


──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────